## Eos, Dawn of Light -- A Space Opera
## Copyright (c) 2007 Casey Duncan and contributors
## See LICENSE.txt for licensing details

# Two dimensional vectors
# $Id: vector.py 85 2007-02-09 05:40:02Z oberon7 $

import math

# number of cached unit vectors
unit_vectors = 360 
max_radian = 2 * math.pi
unit_vector_res = max_radian / unit_vectors

class Vector2D(object):
	"""Two dimensional vector

	>>> Vector2D(3, 4).length
	5.0
	>>> Vector2D(100, 0).normal.x
	1.0
	>>> Vector2D(100, 0).normal.y
	0.0
	>>> Vector2D(0, -42).normal.x
	0.0
	>>> Vector2D(0, -42).normal.y
	-1.0
	"""

	def __init__(self, x=0, y=0):
		"""Create a 2 dimensional vector

		>>> v = Vector2D(2, 3)
		>>> v.x, v.y
		(2, 3)
		"""
		self.__x = x
		self.__y = y
		self._clearcache()

	# magic Y property that invalidates this vector's 
	# cache whenever it's modified in place
	def x_fget(self):
		return self.__x
	def x_fset(self, value):
		self._clearcache()
		self.__x = value
	x = property(fget=x_fget, fset=x_fset)

	# magic Y property that invalidates this vector's 
	# cache whenever it's modified in place
	def y_fget(self):
		return self.__y
	def y_fset(self, value):
		self._clearcache()
		self.__y = value
	y = property(fget=y_fget, fset=y_fset)

	def __getitem__(self, index):
		"""Emulate sequence (x, y, 0). The third dimension is supplied
		for easy compatibility with ode, which is 3D
		"""
		return (self.x, self.y, 0)[index]

	def __add__(self, other):
		"""Add two vectors or a vector and a tuple. Return a new vector

		>>> v1 = Vector2D(2, 3)
		>>> v2 = v1 + Vector2D(5, 6)
		>>> v1.x, v1.y
		(2, 3)
		>>> v2.x, v2.y
		(7, 9)
		>>> v3 = v1 + (1, -1)
		>>> v1.x, v1.y
		(2, 3)
		>>> v3.x, v3.y
		(3, 2)
		"""
		if isinstance(other, Vector2D):
			return Vector2D(self.x + other.x, self.y + other.y)
		else:
			return Vector2D(self.x + other[0], self.y + other[1])
	
	def __iadd__(self, other):
		"""Add two vectors or a vector and a tuple in place

		>>> v = Vector2D(3, 4)
		>>> v.length
		5.0
		>>> v += Vector2D(5, 6)
		>>> v.x, v.y
		(8, 10)
		>>> v.length**2
		164.0
		>>> v += (1, -1)
		>>> v.x, v.y
		(9, 9)
		>>> v.length**2
		162.0
		"""
		if isinstance(other, tuple):
			self.x += other[0]
			self.y += other[1]
		else:
			self.x += other.x
			self.y += other.y
		return self

	def __sub__(self, other):
		"""Subtract two vectors or a tuple from a vector. Return a new vector

		>>> v1 = Vector2D(8, 3)
		>>> v2 = v1 - Vector2D(5, 6)
		>>> v1.x, v1.y
		(8, 3)
		>>> v2.x, v2.y
		(3, -3)
		>>> v3 = v1 - (1, -1)
		>>> v1.x, v1.y
		(8, 3)
		>>> v3.x, v3.y
		(7, 4)
		"""
		if isinstance(other, Vector2D):
			return Vector2D(self.x - other.x, self.y - other.y)
		else:
			return Vector2D(self.x - other[0], self.y - other[1])
	
	def __isub__(self, other):
		"""Add two vectors or a vector and a tuple in place

		>>> v = Vector2D(8, 10)
		>>> v.length**2
		164.0
		>>> v -= Vector2D(5, 6)
		>>> v.x, v.y
		(3, 4)
		>>> v.length
		5.0
		>>> v -= (3, 1)
		>>> v.x, v.y
		(0, 3)
		>>> v.length
		3.0
		"""
		if isinstance(other, tuple):
			self.x -= other[0]
			self.y -= other[1]
		else:
			self.x -= other.x
			self.y -= other.y
		return self

	def __div__(self, k):
		return Vector2D(self.x / k, self.y / k)
	
	def __idiv__(self, k):
		self.x /= k
		self.y /= k
		return self

	def __mul__(self, k):
		return Vector2D(self.x * k, self.y * k)
	
	def __imul__(self, k):
		self.x *= k
		self.y *= k
		return self

	def __neg__(self):
		return Vector2D(-self.x, -self.y)
	
	def __eq__(self, other):
		"""Compare two vectors for equality

		>>> Vector2D(4, 2) == Vector2D(4, 2)
		True
		>>> Vector2D(4, 2) == Vector2D(2, 4)
		False
		"""
		return self.x == other.x and self.y == other.y

	def clamp(self, k):
		if self.length > k:
			return self * (k / self.length)
		return self
	
	def clamp_ip(self, k):
		"""Clamp vector to length in place"""
		if self.length > k:
			self *= (k / self.length)

	@property
	def radians(self):
		if self._radians is None:
			self._radians = math.atan2(self.y, self.x) % fullcircle
		return self._radians

	def distance(self, other):
		return (self - other).length

	@property
	def length(self):
		# a^2 + b^2 = c^2
		if self._length is None:
			self._length = math.sqrt(self.x**2 + self.y**2)
		return self._length
	
	def dotprod(self, other):
		"""Compute the dot product of two vectors returning the
		scalar value
		"""
		theta = self.radians - other.radians
		return self.length * other.length * math.cos(theta)

	@property
	def normal(self):
		if self._normal is None:
			if self.length == 0:
				self._normal = Vector2D()
			else:
				self._normal = self / self.length
		return self._normal
	
	def copy(self):
		"""Return a new vectory equal to self

		>>> v = Vector2D(5, 6)
		>>> v2 = v.copy()
		>>> v.x == v2.x and v.y == v2.y and v.length == v2.length
		True
		"""
		cp = Vector2D(self.x, self.y)
		cp._radians = self._radians
		cp._length = self._length
		cp._normal = self._normal
		return cp

	@classmethod
	def unit(cls, radians):
		"""Return the unit vector for a given heading in radians

		>>> Vector2D.unit(0).radians
		0.0
		>>> Vector2D.unit(math.pi).radians == math.pi
		True
		>>> Vector2D.unit(math.pi*2).radians
		0.0
		"""
		return _heading_vec[int(radians / unit_vector_res) % unit_vectors]
	
	def _clearcache(self):
		"""Invalidate cached properties"""
		self._radians = None
		self._length = None
		self._normal = None
	
	def __nonempty__(self):
		return self.x != 0 or self.y != 0
	
	def __repr__(self):
		return "<%s(%s, %s) at %x>" % (
			self.__class__.__name__, self.x, self.y, id(self))

class SyncedVector(Vector2D):
	"""Vector synchronized to external """

	def __init__(self, getter, setter):
		"""Create vector where the coordinate values are retrieved via the
		getter (which returns a tuple) and set via the setter which accepts a
		sequence with coordinate values

		>>> class VecSource:
		...   vec = (0, 0, 0)
		...   def get(self):
		...     return tuple(self.vec)
		...   def set(self,v):
		...     self.vec = tuple(v)
		...
		>>> src = VecSource()
		>>> sv = SyncedVector(src.get, src.set)
		>>> sv.x, sv.y
		(0, 0)
		>>> sv.length
		0.0
		>>> sv.x = 3
		>>> sv.y = 4
		>>> sv.x, sv.y
		(3, 4)
		>>> sv[0], sv[1]
		(3, 4)
		>>> sv.length
		5.0
		>>> src.vec
		(3, 4, 0)
		>>> sv.x *= 2
		>>> sv.x, sv.y
		(6, 4)
		>>> sv.length == Vector2D(6, 4).length
		True
		>>> src.vec
		(6, 4, 0)
		"""
		self.getter = getter
		self.setter = setter
		self.sync()
	
	def sync(self):
		"""Synchronize our coordinate values, retrieving them using the getter
		"""
		self._clearcache()
		self._vec = list(self.getter())

	def x_fget(self):
		return self._vec[0]
	def x_fset(self, value):
		self._clearcache()
		self._vec[0] = value
		self.setter(self._vec)
	x = property(fget=x_fget, fset=x_fset)

	def y_fget(self):
		return self._vec[1]
	def y_fset(self, value):
		self._clearcache()
		self._vec[1] = value
		self.setter(self._vec)
	y = property(fget=y_fget, fset=y_fset)

	def __getitem__(self, index):
		return self._vec[index]

class PositionVector(SyncedVector):
	"""Vector bound to ode body position"""

	def __init__(self, body):
		"""Create vector bound to body, an ode Body object"""
		SyncedVector.__init__(self, body.getPosition, body.setPosition)

class VelocityVector(SyncedVector):
	"""Vector bound to ode body velocity"""

	def __init__(self, body):
		"""Create vector bound to body, an ode Body object"""
		SyncedVector.__init__(self, body.getLinearVel, body.setLinearVel)

# angle aliases
fullcircle = math.pi * 2
halfcircle = math.pi
rightangle = math.pi / 2
diagonal = math.pi / 4

# Cached unit vector map
_heading_vec = {}
for d in range(unit_vectors):
	r = d * unit_vector_res
	uv = Vector2D(math.cos(r), math.sin(r))
	_heading_vec[d] = uv


if __name__ == '__main__':
	"""Run tests if executed directly"""
	import sys, doctest
	failed, count = doctest.testmod()
	print 'Ran', count, 'test cases with', failed, 'failures'
	sys.exit(failed)

